NetOps Coding#1(ネットワークAPI作成30分Coding)

  • 27
    Like
  • 0
    Comment
More than 1 year has passed since last update.

NetOps Coding 第一回のネタです。

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

なお、今回使用したサンプルコードはこちらに上げています。

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

用意するもの

  • 仮想ルータ  今回はvSRX(旧firefly)を使用
  • 仮想マシン  今回はIDCFクラウドのVMを使用
  • 言語     今回はRuby
  • Webアプリ  今回はSinatra(Webrick)
  • テストツール DHCPostman
  • DB      今回はMariaDB(Cent6の場合はMySQL)

vSRXのインストール、設定方法はこちらを参照
開発環境のセットアップはこちらを参照
DHCやPostmanはChromeでREST APIをテストするならDeveloperツールです。
開発するならおすすめです :laughing:

やること

  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はこんな感じ

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

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

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

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

設定

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:

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

設定情報流し込んで

    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:

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

設定確認

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

実行

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

削除

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

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:

まとめて削除

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はここらへん参照

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

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"を付けてやればよいだけ :laughing:

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:

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:

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としておく。

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)

サンプルデータ投入

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を作成

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から設定を引っ張ってきて、ルータに設定を入れるプログラムを作ってみます。

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

無事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: