NetOps Coding#1（ネットワークAPI作成30分Coding）

[NetOps Coding 第一回](https://atnd.org/events/70253)のネタです。

30分でコードからRest APIを作成し、仮想ルータにdescriptionの設定を入れてみます。
description設定して何になるかって？descriptionが設定できれば、あとは何でも設定できるのです :alien: 

なお、今回使用したサンプルコードは[こちら](https://github.com/netops-coding/netops-study)に上げています。

さあ、それでは始めましょう♪

# 用意するもの

- 仮想ルータ　　今回はvSRX（旧firefly）を使用
- 仮想マシン　　今回はIDCFクラウドのVMを使用
- 言語　　　　　今回はRuby
- Webアプリ　　今回はSinatra(Webrick)
- テストツール　[DHC](https://chrome.google.com/webstore/detail/dhc-resthttp-api-client/aejoelaoggembcahagimdiliamlcdmfm)、[Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop)
- DB　　　　　　今回はMariaDB（Cent6の場合はMySQL)

vSRXのインストール、設定方法は[こちらを参照](http://qiita.com/inoueissei/items/b60a54a7a4528f4174ab)
開発環境のセットアップは[こちらを参照](http://qiita.com/inoueissei/items/d81f6f8f706966af4630)
DHCやPostmanはChromeでREST APIをテストするならDeveloperツールです。
開発するならおすすめです :satisfied: 


# やること

1. rubyでNetconfを使って仮想ルータ（vSRX）に設定を入れてみる
2. ソフトウェアの強みを活かして沢山入れてみる
3. DBにルータの設定情報を入れ込む
4. APIを作成して、APIを叩いて仮想ルータに設定を投入する

# 前置き（コードからルータに設定を入れる方法）

コードからネットワーク機器に設定を入れる方法は大きく3つあると思います。

1. RestAPI
2. Netconf
3. telnet＆expect駆使

1.のRestAPIがコードを書く上では一番やり易いですが、RestAPIをサポートしているネットワーク機器は少ない。F5のBIG-IP(ver11.5以降)、Arista、Cumulusなどはあるが、まだ少ない

ちなみにBIG-IPのRestAPIは[こんな感じ](http://qiita.com/inoueissei/items/8fbb6c4f9f8741ed67e1)

2.のNetconfはLibraryが提供されていれば使えるが、まともに出ているものは少ない :sob: 
Juniperルータに関しては[juniper/netconf](https://github.com/Juniper/net-netconf)というのがあったので、これを使った。
しかしこれも2013年以降アップデートがなく、成熟度は高くないように思われる。
また、メーカさんからしてみれば当然なのでしょうが、他のメーカの機種ではこれを使うことはまずできない :weary: 

**今回の勉強会を通して、ネットワークエンジニアがコードをどんどん書いていって、メーカの方々もソフトウェアで機器を扱いやすいようなLibraryどんどんを作って、それをforkして、といったサイクルが回っていくことを切に願っています**:pray:


# rubyでNetconfを使って仮想ルータ（vSRX）に設定を入れてみる

まず、ただただ、interface ge-0/0/0.1にvlan 1, description netops1030を入れる設定をやってみる。

## 設定

```rb
require "net/netconf"

Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
  puts device.rpc.lock(:candidate)
  puts device.rpc.edit_config {|x|
    x.configuration {
      x.interfaces {
        x.interface {
          x.name "ge-0/0/0"
          x.unit {
            x.name "1"
            x.description "description_netops1030"
            x.send("vlan-id",1)
          }
        }
      }
    }
  }
  puts device.rpc.validate(:candidate)
  puts device.rpc.commit
  puts device.rpc.unlock :candidate
end
```

実行してみる :kissing: 

```xml
# ruby set_ifdescription.rb
<ok/>
<ok/>
<commit-results>
</commit-results>
<ok/>
<ok/>
```

vSRX側の確認

```
# show | compare rollback 1
[edit interfaces ge-0/0/0]
+    unit 1 {
+        description description_netops1030;
+        vlan-id 1;
+    }
```

バッチリ :tada: 


## 振り返って確認

sshでnetconfでログインして、
`Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|`
lockして、
`  puts device.rpc.lock(:candidate)`
editモードになって
`  puts device.rpc.edit_config {|x|`

設定情報流し込んで

```rb
    x.configuration {
      x.interfaces {
        x.interface {
          x.name "ge-0/0/0"
          x.unit {
            x.name "1"
            x.description "description_netops1030"
            x.send("vlan-id",1)
```

validateして
`  puts device.rpc.validate(:candidate)`
commitして
`  puts device.rpc.commit`
unlockしている。
`  puts device.rpc.unlock :candidate`

rubyでは**"-"（ハイフン）は特別な文字扱いをされてしまう**ため、vlan-idのようなものはsend("vlan-id")といった形で**send()で囲ってやる**必要がある
やったらx.hogehogeといったものを記述しているが、netconfではXML形式で情報を投げるため、そのためにこんな記述になっているのであろう :neckbeard: 

Juniperの場合は`show interfaces | display xml`などとすると、下記な感じでxml形式での情報が表示されるので、コード書くときにはかなり参考になる。ここら辺がJuniperの良いところ :heart: 

```xml
# show interfaces | display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1X49/junos">
    <configuration junos:changed-seconds="1446099446" junos:changed-localtime="2015-10-29 15:17:26 JST">
            <interfaces>
                <interface>
                    <name>ge-0/0/0</name>
                    <flexible-vlan-tagging/>
                    <native-vlan-id>0</native-vlan-id>
                    <unit>
                        <name>0</name>
                        <vlan-id>0</vlan-id>
                        <family>
                            <inet>
                                <dhcp>
                                </dhcp>
                            </inet>
                        </family>
                    </unit>
                    <unit>
                        <name>1</name>
                        <description>description_netops1030</description>
                        <vlan-id>1</vlan-id>
                    </unit>
                </interface>
                <interface>
                    <name>ge-0/0/1</name>
                    <flexible-vlan-tagging/>
                    <native-vlan-id>0</native-vlan-id>
                    <unit>
                        <name>0</name>
                        <vlan-id>0</vlan-id>
                        <family>
                            <inet>
                                <dhcp>
                                </dhcp>
                            </inet>
                        </family>
                    </unit>
                </interface>
                <interface>
                    <name>fxp0</name>
                    <unit>
                        <name>0</name>
                        <family>
                            <inet>
                                <address>
                                    <name>10.6.0.33/21</name>
                                </address>
                            </inet>
                        </family>
                    </unit>
                </interface>
            </interfaces>
    </configuration>
    <cli>
        <banner>[edit]</banner>
    </cli>
</rpc-reply>
```

## 設定確認

```rb
Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
  show = device.rpc.get_interface_information( :interface_name => "ge-0/0/0.1", :detail => true )
  name = show.xpath("//name")
  desc = show.xpath("//description")
  show_summary = name + desc
  puts show_summary
end
```

実行

```rb
# ruby get_interface.rb
<name>
ge-0/0/0.1
</name>
<description>
description_netops1030
</description>
```

## 削除

消したい部分で**("operation"=>"delete")**を入れる

```rb
Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
  puts device.rpc.lock(:candidate)
  puts device.rpc.edit_config {|x|
    x.configuration {
      x.interfaces {
        x.interface {
          x.name "ge-0/0/0"
          x.unit("operation"=>"delete"){
            x.name "1"
          }
        }
      }
    }
  }
  puts device.rpc.validate(:candidate)
  puts device.rpc.commit
  puts device.rpc.unlock :candidate
end
```

# ソフトウェアの強みを活かして沢山入れてみる

単にfor文回せば良い。
ただし、どこにfor入れるかはちょっと注意が必要 :persevere: 

interface ge-0/0/0.1～10に設定
（本当は1000個くらい入れたかったんですが、vSRXが重すぎるので断念。。 :sweat: ）

## まとめて削除

```rb
require "net/netconf"

Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
  puts device.rpc.lock(:candidate)
  for i in 1..10
  puts device.rpc.edit_config {|x|
    x.configuration {
      x.interfaces {
        x.interface {
          x.name "ge-0/0/0"
          x.unit("operation"=>"delete"){
            x.name "#{i}"
          }
        }
      }
    }
  }
  end
  puts device.rpc.validate(:candidate)
  puts device.rpc.commit
  puts device.rpc.unlock :candidate
end
```


## set形式での記述
xml形式で記述するのシンドイ！！！と思ってたら、フツーに入れる方法もありました :confetti_ball: 
ただし`require "net/netconf/jnpr"`が必要
rpcの記述も変える必要があります。
sampleは[ここらへん参照](https://github.com/Juniper/net-netconf/blob/f2dd271ddebf7416694a254c7407045a7b31e973/examples/jnpr/edit-config-jnpr-set.rb)

さっきの長ったらしい記述がたったこれだけ！且つ見慣れたCLI :heart_eyes: 
先に言えよ！って感じですよね :space_invader: 

```rb
require "net/netconf"
require "net/netconf/jnpr"

Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
  puts device.rpc.lock_configuration
  puts device.rpc.load_configuration( :format => 'set' ) {
    "set interfaces ge-0/0/0 unit 1 vlan-id 1 description netopscoding1_cli"
  }
  puts device.rpc.check_configuration
  puts device.rpc.commit_configuration
  puts device.rpc.unlock_configuration
end
```

複数行まとめて設定する場合も"\n"を付けてやればよいだけ :satisfied: 

```rb
Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
  puts device.rpc.lock_configuration
  puts device.rpc.load_configuration( :format => 'set' ) {
    "set interfaces ge-0/0/0 unit 1 vlan-id 1 description netopscoding1_cli\n
    set interfaces ge-0/0/0 unit 2 vlan-id 2 description netopscoding2_cli"
  }
  puts device.rpc.check_configuration
  puts device.rpc.commit_configuration
  puts device.rpc.unlock_configuration
end
```

for文もより自由な形で記述できます :sunglasses: 

```rb
require "net/netconf"
require "net/netconf/jnpr"

config = ""
for i in 1..10
  config << "set interfaces ge-0/0/0 unit #{i} vlan-id #{i} description netopscoding#{i}_cli\n"
end

Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
  puts device.rpc.lock_configuration
  puts device.rpc.load_configuration( :format => 'set' ) {
    config
  }
  puts device.rpc.check_configuration
  puts device.rpc.commit_configuration
  puts device.rpc.unlock_configuration
end
```

# DBにルータの設定情報を入れ込む

次はDB作成です。既に随分長い資料になってしまいました :sweat_drops: 

```mysql
MariaDB [(none)]> create database netops_codings;
Query OK, 1 row affected (0.00 sec)

MariaDB [(none)]> use netops_codings
Database changed
```

table作成
※**Rubyではtable名をスネークケース且つ複数形**に指定されるので、table名はnetops_codingsとしておく。

```mysql
create table netops_codings (
    -> id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    -> unit INT NOT NULL,
    -> vlan INT NOT NULL,
    -> description VARCHAR(255) NOT NULL,
    -> created_at DATETIME NOT NULL,
    -> updated_at DATETIME NOT NULL,
    -> PRIMARY KEY(id)
    -> );
Query OK, 0 rows affected (0.00 sec)
```

サンプルデータ投入

```mysql
insert into netops_codings values(1, 1, 1, "netops1", now(), now());
insert into netops_codings values(2, 2, 2, "netops2", now(), now());
insert into netops_codings values(3, 3, 3, "netops3", now(), now());
insert into netops_codings values(4, 4, 4, "netops4", now(), now());
insert into netops_codings values(5, 5, 5, "netops5", now(), now());

MariaDB [netops_codings]> select * from netops_codings ;
+----+------+------+-------------+---------------------+---------------------+
| id | unit | vlan | description | created_at          | updated_at          |
+----+------+------+-------------+---------------------+---------------------+
|  1 |    1 |    1 | netops1     | 2015-10-29 21:15:13 | 2015-10-29 21:15:13 |
|  2 |    2 |    2 | netops2     | 2015-10-29 21:16:02 | 2015-10-29 21:16:02 |
|  3 |    3 |    3 | netops3     | 2015-10-29 21:16:02 | 2015-10-29 21:16:02 |
|  4 |    4 |    4 | netops4     | 2015-10-29 21:16:02 | 2015-10-29 21:16:02 |
|  5 |    5 |    5 | netops5     | 2015-10-29 21:16:02 | 2015-10-29 21:16:02 |
+----+------+------+-------------+---------------------+---------------------+
5 rows in set (0.00 sec)
```

プログラムからDBにアクセスするためにdatabase.ymlを作成

```yaml
development:
  adapter: mysql2
  database: netops_codings
  host: localhost
  username: <%= ENV['DB_USER'] %>
  password: <%= ENV['DB_PASSWORD'] %>
  encoding: utf8
```

# APIを作成して、APIを叩いて仮想ルータに設定を投入する

それでは、いよいよ、Web APIでjsonでidを渡して、DBのIDから設定を引っ張ってきて、ルータに設定を入れるプログラムを作ってみます。

```rb
require "active_record"
require "mysql2"
require "sinatra"
require "sinatra/reloader"
require "net/netconf"
require "net/netconf/jnpr"
require "erb"

database = File.read("database.yml")

# DB設定ファイルの読み込み
# 環境変数付きのymlファイルをそのままload_fileすることはできないので、ERBに渡して環境変数を取り出してからloadする必要がある。
ActiveRecord::Base.configurations = YAML.load(ERB.new(database).result)
ActiveRecord::Base.establish_connection(:development)

# クラス作成
class SetInterface
  def initialize(id)
    @id = id
  end

# これがスネークケースの複数形（netops_codings）になり、table名と一致していないといけない
  class NetopsCoding < ActiveRecord::Base
  end

  def set_interface
    value = NetopsCoding.find_by id: (@id)
    id = value.id
    unit = value.unit
    vlan = value.unit
    description = value.description


Netconf::SSH.new(target: ENV['ROUTER_IP'], username: ENV['ROUTER_USER'], password: ENV['ROUTER_PASSWORD']) do |device|
      puts device.rpc.lock(:candidate)
      puts device.rpc.load_configuration( :format => "set" ) {
        "set interfaces ge-0/0/0 unit #{unit} vlan-id #{vlan} description #{description}"
      }
      puts device.rpc.validate(:candidate)
      puts device.rpc.commit
      puts device.rpc.unlock :candidate
    end
  end
end

post "/set_interface" do
  # HTTP Request解析
  reqData = JSON.parse(request.body.read.to_s)
  id = reqData["id"]
  set_config = SetInterface.new(id)
  set_config.set_interface
  status 202
end

get "/" do
  "Hello NetOps Coding#1"
end
```

最期にsinatraを起動させて、APIを受け付けられるようにします。
デフォルトでは4567ポートを使うので、とりあえず80に変更。あと外部からsinatraに接続させるためには"-o 0.0.0.0"が必要となります。
（本当はsupervisordとか使ってdaemon化した方が良いんでしょうけど割愛 :skull: ）

`ruby network_api.rb -p 80 -o 0.0.0.0`

こんな感じでDHCを使って、POSTで/set_interfaceにアクセスし、JSONで"id": "1"を渡してやります。

![image](https://qiita-image-store.s3.amazonaws.com/0/53465/80bf6ca5-1159-4149-288d-b421d9c63eb3.png)

無事202 Acceptedが返ってきて・・

```
netops# show | compare rollback 1
[edit interfaces ge-0/0/0]
+    unit 1 {
+        description netops1;
+        vlan-id 1;
+    }

[edit]
```

ルータに設定も反映されています :tada: 

設定削除や、show関連の確認、DBへのレコード追加などもSinatraを使えば同じようにできます :sunglasses: 

今回はこれで以上です
長時間お付き合いいただきありがとうございました :bowtie: